const fs = require('fs');
const path = require('path');

const crc32 = require('./crc32');

const PNG_SIGNATURE_BYTES = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);

function checkType(buffer) {
    if (!(buffer instanceof Buffer) || buffer.length === 0) {
        throw new Error('buffer is empty');
    }

    PNG_SIGNATURE_BYTES.forEach((bytes, index) => {
        if (bytes !== buffer[index]) {
            throw new Error('Not a PNG file (invalid file signature)');
        }
    });

    return true;
}

function readDWord(bytes, off) {
    let x = 0;
    // Force the most-significant byte to unsigned.
    x += ((bytes[0 + off] << 24) >>> 0);
    for (let i = 1; i < 4; i++) x += ((bytes[i + off] << ((3 - i) * 8)));
    return x;
}

function readWord(bytes, off) {
    let x = 0;
    for (let i = 0; i < 2; i++) x += (bytes[i + off] << ((1 - i) * 8));
    return x;
}

function readString(bytes, off, length) {
    const chars = Array.prototype.slice.call(bytes.subarray(off, off + length));
    // eslint-disable-next-line prefer-spread
    return String.fromCharCode.apply(String, chars);
}

function readByte(bytes, off) {
    return bytes[off];
}

function subBuffer(bytes, start, length) {
    return bytes.subarray(start, start + length);
}

function makeDWordArray(x) {
    return [(x >>> 24) & 0xff, (x >>> 16) & 0xff, (x >>> 8) & 0xff, x & 0xff];
}

function makeStringArray(x) {
    const res = [];
    for (let i = 0; i < x.length; i++) res.push(x.charCodeAt(i));
    return res;
}

function makeChunkBytes(type, dataBytes) {
    const crcLen = type.length + dataBytes.length;
    const bytes = new Uint8Array(new ArrayBuffer(crcLen + 8));
    bytes.set(makeDWordArray(dataBytes.length), 0);
    bytes.set(makeStringArray(type), 4);
    bytes.set(dataBytes, 8);
    const crc = crc32(bytes, 4, crcLen);
    bytes.set(makeDWordArray(crc), crcLen + 4);
    return Buffer.from(bytes);
}

function parseChunks(bytes, callback) {
    let off = 8;

    let type,
        res;
    do {
        const length = readDWord(bytes, off);
        type = readString(bytes, off + 4, 4);
        res = callback(type, bytes, off, length);
        off += 12 + length;
    } while (res !== false && type !== 'IEND' && off < bytes.length);
}

module.exports = {
    apngToPngs(buffer) {
        checkType(buffer);

        let isAnimated = false;
        parseChunks(buffer, type => {
            if (type === 'acTL') {
                isAnimated = true;
                return false;
            }
            return true;
        });

        if (!isAnimated) {
            throw new Error('Not an animated PNG');
        }

        const preDataParts = [],
            postDataParts = [],
            anim = {
                width: 0,
                height: 0,
                playTime: 0,
                frames: []
            };
        let headerDataBytes = null,
            frame = null;
        parseChunks(buffer, (type, bytes, off, length) => {
            switch (type) {
            case 'IHDR':
                headerDataBytes = bytes.subarray(off + 8, off + 8 + length);
                anim.width = readDWord(bytes, off + 8);
                anim.height = readDWord(bytes, off + 12);
                break;
            case 'acTL':
                anim.numPlays = readDWord(bytes, off + 8 + 4);
                break;
            case 'fcTL':
                if (frame) anim.frames.push(frame);
                frame = {};
                frame.width = readDWord(bytes, off + 8 + 4);
                frame.height = readDWord(bytes, off + 8 + 8);
                frame.left = readDWord(bytes, off + 8 + 12);
                frame.top = readDWord(bytes, off + 8 + 16);

                // eslint-disable-next-line no-case-declarations
                const delayN = readWord(bytes, off + 8 + 20);
                // eslint-disable-next-line no-case-declarations
                let delayD = readWord(bytes, off + 8 + 22);

                if (delayD === 0) delayD = 100;
                frame.delay = (1000 * delayN) / delayD;
                if (frame.delay <= 10) frame.delay = 100;
                anim.playTime += frame.delay;
                frame.disposeOp = readByte(bytes, off + 8 + 24);
                frame.blendOp = readByte(bytes, off + 8 + 25);
                frame.dataParts = [];
                break;
            case 'fdAT':
                if (frame) frame.dataParts.push(bytes.subarray(off + 8 + 4, off + 8 + length));
                break;
            case 'IDAT':
                if (frame) frame.dataParts.push(bytes.subarray(off + 8, off + 8 + length));
                break;
            case 'IEND':
                postDataParts.push(subBuffer(bytes, off, 12 + length));
                break;
            default:
                preDataParts.push(subBuffer(bytes, off, 12 + length));
            }
        });

        if (frame) anim.frames.push(frame);

        if (anim.frames.length === 0) {
            throw new Error('Not an animated PNG');
        }

        // eslint-disable-next-line max-len
        const preBlob = Buffer.concat(preDataParts, preDataParts.reduce((acc, curr) => acc + curr.length, 0));
        // eslint-disable-next-line max-len
        const postBlob = Buffer.concat(postDataParts, postDataParts.reduce((acc, curr) => acc + curr.length, 0));
        for (let f = 0; f < anim.frames.length; f++) {
            frame = anim.frames[f];

            const bb = [];
            bb.push(PNG_SIGNATURE_BYTES);
            headerDataBytes.set(makeDWordArray(frame.width), 0);
            headerDataBytes.set(makeDWordArray(frame.height), 4);
            bb.push(makeChunkBytes('IHDR', headerDataBytes));
            bb.push(preBlob);
            for (let j = 0; j < frame.dataParts.length; j++) {
                bb.push(makeChunkBytes('IDAT', frame.dataParts[j]));
            }
            bb.push(postBlob);
            const imgBuf = Buffer.concat(bb, bb.reduce((acc, curr) => acc + curr.length, 0));

            frame.buffer = imgBuf;
            frame.filename = path.resolve(__dirname, `../caches/${f}.png`);
            fs.writeFileSync(frame.filename, imgBuf);
        }

        return anim.frames;
    }
};
